/**
 * @author nttdocomo
 */
define(function(require) {
	require('./picker');
	require('../../util/bindable');
	require('../../view/boundList');
	require('../../view/boundListKeyNav');
	aries.klass("aries.form.field.ComboBox", aries.form.field.Picker.extend({
		/**
		 * @cfg {Boolean} autoSelect
		 * `true` to automatically highlight the first result gathered by the data store in the dropdown list when it is
		 * opened. A false value would cause nothing in the list to be highlighted automatically, so
		 * the user would have to manually highlight an item before pressing the enter or {@link #selectOnTab tab} key to
		 * select it (unless the value of ({@link #typeAhead}) were true), or use the mouse to select a value.
		 */
		autoSelect : true,

		/**
		 * @cfg {Object} defaultListConfig
		 * Set of options that will be used as defaults for the user-configured {@link #listConfig} object.
		 */
		defaultListConfig : {
			loadingHeight : 70,
			minWidth : 70,
			maxHeight : 300,
			shadow : 'sides'
		},

		/**
		 * @cfg {String} delimiter
		 * The character(s) used to separate the {@link #displayField display values} of multiple selected items when
		 * `{@link #multiSelect} = true`.
		 */
		//<locale>
		delimiter : ', ',
		fieldSubTpl : ['<div class="<%=hiddenDataCls%>" role="presentation"></div>', '<input id="<%=id%>" type="<%=type%>" class="<%=fieldCls%> <%=typeCls%>" autocomplete="off"', '<%if(typeof(value) !== "undefined"){%><%=_.escape(value)%><%}%>', '<%if(typeof(name) !== "undefined"){%><%=name%><%}%>', '<%if(typeof(placeholder) !== "undefined"){%><%=placeholder%><%}%>', '<%if(typeof(size) !== "undefined"){%><%=size%><%}%>', '<%if(typeof(maxLength) !== "undefined"){%><%=maxLength%><%}%>', '<%if(typeof(readOnly) !== "undefined"){%><%=readOnly%><%}%>', '<%if(typeof(disabled) !== "undefined"){%><%=disabled%><%}%>', '<%if(typeof(tabIdx) !== "undefined"){%><%=tabIdx%><%}%>', '<%if(typeof(fieldStyle) !== "undefined"){%><%=fieldStyle%><%}%>', '/>'].join(''),

		/**
		 * @private
		 * @cfg {String}
		 * CSS class used to find the {@link #hiddenDataEl}
		 */
		hiddenDataCls : aries.baseCSSPrefix + 'hide-display ' + aries.baseCSSPrefix + 'form-data-hidden',
		/**
		 * @cfg {String} [triggerCls='x-form-arrow-trigger']
		 * An additional CSS class used to style the trigger button. The trigger will always get the {@link #triggerBaseCls}
		 * by default and `triggerCls` will be **appended** if specified.
		 */
		triggerCls : aries.baseCSSPrefix + 'form-arrow-trigger',

		alignPicker : function() {
			var me = this, picker = me.getPicker(), heightAbove = me.getPosition().top - aries.$body.scrollTop(), heightBelow = $(window).height() - heightAbove - me.getHeight(), space = Math.max(heightAbove, heightBelow);

			// Allow the picker to height itself naturally.
			if (picker.height) {
				delete picker.height;
				picker.updateLayout();
			}
			// Then ensure that vertically, the dropdown will fit into the space either above or below the inputEl.
			if (picker.getHeight() > space - 5) {
				picker.setHeight(space - 5);
				// have some leeway so we aren't flush against
			}
			aries.form.field.Picker.prototype.alignPicker.apply(this, arguments);
		},

		beforeBlur : function() {
			//this.doQueryTask.cancel();
			//this.assertValue();
		},

		createPicker : function() {
			var me = this, picker, menuCls = aries.baseCSSPrefix + 'menu', pickerCfg = $.extend({
				xtype : 'boundlist',
				pickerField : me,
				selModel : {
					mode : me.multiSelect ? 'SIMPLE' : 'SINGLE'
				},
				floating : true,
				hidden : true,

				// The picker (the dropdown) must have its zIndex managed by the same ZIndexManager which is
				// providing the zIndex of our Container.
				ownerCt : me.$el.parent('[floating]'),
				cls : me.el && me.$el.parent('.' + menuCls) ? menuCls : '',
				store : me.store,
				displayField : me.displayField,
				focusOnToFront : false,
				pageSize : me.pageSize,
				tpl : me.tpl
			}, me.listConfig, me.defaultListConfig);

			picker = me.picker = new aries.view.BoundList(pickerCfg)//Ext.widget(pickerCfg);
			if (me.pageSize) {
				picker.pagingToolbar.on('beforechange', me.onPageChange, me);
			}

			picker.on('itemclick', me.onItemClick, me);
			picker.on('refresh', me.onListRefresh, me);
			/*me.mon(picker, {
			 itemclick : me.onItemClick,
			 refresh : me.onListRefresh,
			 scope : me
			 });*/

			picker.getSelectionModel().on('beforeselect', me.onBeforeSelect, me);
			picker.getSelectionModel().on('beforedeselect', me.onBeforeDeselect, me);
			picker.getSelectionModel().on('selectionchange', me.onListSelectionChange, me);

			/*me.mon(picker.getSelectionModel(), {
			 beforeselect : me.onBeforeSelect,
			 beforedeselect : me.onBeforeDeselect,
			 selectionchange : me.onListSelectionChange,
			 scope : me
			 });*/

			return picker;
		},

		/**
		 * @private
		 * If the autoSelect config is true, and the picker is open, highlights the first item.
		 */
		doAutoSelect : function() {
			var me = this, picker = me.picker, lastSelected, itemNode;
			if (picker && me.autoSelect && me.store.length > 0) {
				// Highlight the last selected item and scroll it into view
				lastSelected = picker.getSelectionModel().lastSelected;
				itemNode = picker.getNode(lastSelected || 0);
				if (itemNode) {
					picker.highlightItem(itemNode);
					itemNode.scrollIntoView(false);
				}
			}
		},

		/**
		 * Executes a query to filter the dropdown list. Fires the {@link #beforequery} event prior to performing the query
		 * allowing the query action to be canceled if needed.
		 *
		 * @param {String} queryString The SQL query to execute
		 * @param {Boolean} [forceAll=false] `true` to force the query to execute even if there are currently fewer characters in
		 * the field than the minimum specified by the `{@link #minChars}` config option. It also clears any filter
		 * previously saved in the current store.
		 * @param {Boolean} [rawQuery=false] Pass as true if the raw typed value is being used as the query string. This causes the
		 * resulting store load to leave the raw value undisturbed.
		 * @return {Boolean} true if the query was permitted to run, false if it was cancelled by a {@link #beforequery}
		 * handler.
		 */
		doQuery : function(queryString, forceAll, rawQuery) {
			queryString = queryString || '';

			// store in object and pass by reference in 'beforequery'
			// so that client code can modify values.
			var me = this, qe = {
				query : queryString,
				forceAll : forceAll,
				combo : me,
				cancel : false
			}, store = me.store, isLocalMode = me.queryMode === 'local', needsRefresh;

			if (me.trigger('beforequery', qe) === false || qe.cancel) {
				return false;
			}

			// get back out possibly modified values
			queryString = qe.query;
			forceAll = qe.forceAll;

			// query permitted to run
			if (forceAll || (queryString.length >= me.minChars)) {
				// expand before starting query so LoadMask can position itself correctly
				me.expand();

				// make sure they aren't querying the same thing
				/*if (!me.queryCaching || me.lastQuery !== queryString) {
				 me.lastQuery = queryString;

				 if (isLocalMode) {
				 // forceAll means no filtering - show whole dataset.
				 store.suspendEvents();
				 needsRefresh = me.clearFilter();
				 if (queryString || !forceAll) {
				 me.activeFilter = new Ext.util.Filter({
				 root : 'data',
				 property : me.displayField,
				 value : queryString
				 });
				 store.filter(me.activeFilter);
				 needsRefresh = true;
				 } else {
				 delete me.activeFilter;
				 }
				 store.resumeEvents();
				 if (me.rendered && needsRefresh) {
				 me.getPicker().refresh();
				 }
				 } else {
				 // Set flag for onLoad handling to know how the Store was loaded
				 me.rawQuery = rawQuery;

				 // In queryMode: 'remote', we assume Store filters are added by the developer as remote filters,
				 // and these are automatically passed as params with every load call, so we do *not* call clearFilter.
				 if (me.pageSize) {
				 // if we're paging, we've changed the query so start at page 1.
				 me.loadPage(1);
				 } else {
				 store.load({
				 params : me.getParams(queryString)
				 });
				 }
				 }
				 }

				 // Clear current selection if it does not match the current value in the field
				 if (me.getRawValue() !== me.getDisplayValue()) {
				 me.ignoreSelection++;
				 me.picker.getSelectionModel().deselectAll();
				 me.ignoreSelection--;
				 }*/

				if (isLocalMode) {
					me.doAutoSelect();
				}
				/*if (me.typeAhead) {
				 me.doTypeAhead();
				 }*/
			}
			return true;
		},

		/**
		 * Finds the record by searching for a specific field/value combination.
		 * @param {String} field The name of the field to test.
		 * @param {Object} value The value to match the field against.
		 * @return {Ext.data.Model} The matched record or false.
		 */
		findRecord : function(field, value) {
			var ds = this.store, idx = ds.findExact(field, value);
			return idx !== -1 ? ds.getAt(idx) : false;
		},

		/**
		 * Finds the record by searching values in the {@link #valueField}.
		 * @param {Object} value The value to match the field against.
		 * @return {Ext.data.Model} The matched record or false.
		 */
		findRecordByValue : function(value) {
			return this.findRecord(this.valueField, value);
		},

		getStoreListeners : function() {
			var me = this;

			return {
				'beforeload' : me.onBeforeLoad,
				'clear' : me.onClear,
				'datachanged' : me.onDataChanged,
				'load' : me.onLoad,
				'exception' : me.onException,
				'remove' : me.onRemove
			};
		},

		getSubTplData : function() {
			var me = this;
			$.extend(me.subTplData, {
				hiddenDataCls : me.hiddenDataCls
			});
			return aries.form.field.Picker.prototype.getSubTplData.apply(this, arguments);
		},
		initComponent : function() {
			var me = this, store = me.store, transform = me.transform, transformSelect, isLocalMode;
			$.extend(me.renderSelectors, {
				hiddenDataEl : '.' + me.hiddenDataCls.split(' ').join('.')
			});

			// Build store from 'transform' HTML select element's options
			if (transform) {
				transformSelect = Ext.getDom(transform);
				if (transformSelect) {
					if (!me.store) {
						store = Ext.Array.map(Ext.Array.from(transformSelect.options), function(option) {
							return [option.value, option.text];
						});
					}
					if (!me.name) {
						me.name = transformSelect.name;
					}
					if (!('value' in me)) {
						me.value = transformSelect.value;
					}
				}
			}

			if (_.isUndefined(me.valueField)) {
				me.valueField = me.displayField;
			}

			isLocalMode = me.queryMode === 'local';
			if (_.isUndefined(me.queryDelay)) {
				me.queryDelay = isLocalMode ? 10 : 500;
			}
			if (_.isUndefined(me.minChars)) {
				me.minChars = isLocalMode ? 0 : 4;
			}

			if (!me.displayTpl) {
				//me.displayTpl = _.template('<tpl for=".">' + '{[typeof values === "string" ? values : values["' + me.displayField + '"]]}' + '<tpl if="xindex < xcount">' + me.delimiter + '</tpl>' + '</tpl>');
				me.displayTpl = _.template('<%_.each(displayTplData,function(item,index){if(typeof(item) === "string"){%><%=item%><%}else{%><%=item.' + me.displayField + '%><%}if(index < displayTplData.length-1){%>' + me.delimiter + '<%}})%>');
			} else if (_.isString(me.displayTpl)) {
				me.displayTpl = _.template(me.displayTpl);
			}
			aries.form.field.Picker.prototype.initComponent.apply(this, arguments)

			// store has already been loaded, setValue
			if (me.store.length > 0) {
				me.setValue(me.value);
			}

			// render in place of 'transform' select
			if (transformSelect) {
				me.render(transformSelect.parentNode, transformSelect);
				Ext.removeNode(transformSelect);
				delete me.renderTo;
			}
		},

		/**
		 * @private Generates the string value to be displayed in the text field for the currently stored value
		 */
		getDisplayValue : function() {
			return this.displayTpl({
				displayTplData : this.displayTplData
			});
		},

		initEvents : function() {
			var me = this;
			aries.form.field.Picker.prototype.initEvents.apply(this, arguments);

			/*
			 * Setup keyboard handling. If enableKeyEvents is true, we already have
			 * a listener on the inputEl for keyup, so don't create a second.
			 */
			/*if (!me.enableKeyEvents) {
			 me.mon(me.inputEl, 'keyup', me.onKeyUp, me);
			 }*/
		},
		/**
		 * @private
		 * Disables the key nav for the BoundList when it is collapsed.
		 */
		onCollapse : function() {
			var me = this, keyNav = me.listKeyNav;
			if (keyNav) {
				keyNav.disable();
				me.ignoreMonitorTab = false;
			}
		},

		/**
		 * @private
		 * Enables the key nav for the BoundList when it is expanded.
		 */
		onExpand : function() {
			var me = this, keyNav = me.listKeyNav, selectOnTab = me.selectOnTab, picker = me.getPicker();

			// Handle BoundList navigation from the input field. Insert a tab listener specially to enable selectOnTab.
			if (keyNav) {
				keyNav.enable();
			} else {
				keyNav = me.listKeyNav = new aries.view.BoundListKeyNav(this.inputEl, {
					boundList : picker,
					forceKeyDown : true,
					tab : function(e) {
						if (selectOnTab) {
							this.selectHighlighted(e);
							me.triggerBlur();
						}
						// Tab key event is allowed to propagate to field
						return true;
					}
				});
			}

			// While list is expanded, stop tab monitoring from Ext.form.field.Trigger so it doesn't short-circuit selectOnTab
			if (selectOnTab) {
				me.ignoreMonitorTab = true;
			}

			//Ext.defer(keyNav.enable, 1, keyNav);
			//wait a bit so it doesn't react to the down arrow opening the picker
			me.inputEl.focus();
		},

		onItemClick : function(picker, record) {
			/*
			 * If we're doing single selection, the selection change events won't fire when
			 * clicking on the selected element. Detect it here.
			 */
			var me = this, selection = me.picker.getSelectionModel().getSelection(), valueField = me.valueField;

			if (!me.multiSelect && selection.length) {
				if (record.get(valueField) === selection[0].get(valueField)) {
					// Make sure we also update the display value if it's only partial
					me.displayTplData = [record.attributes];
					me.setRawValue(me.getDisplayValue());
					me.collapse();
				}
			}
		},

		onListRefresh : function() {
			this.alignPicker();
			this.syncSelection();
		},

		onListSelectionChange : function(list, selectedRecords) {
			var me = this, isMulti = me.multiSelect, hasRecords = selectedRecords.length > 0;
			// Only react to selection if it is not called from setValue, and if our list is
			// expanded (ignores changes to the selection model triggered elsewhere)
			if (!me.ignoreSelection && me.isExpanded) {
				if (!isMulti) {
					me.collapse();
					//Ext.defer(me.collapse, 1, me);
				}
				/*
				 * Only set the value here if we're in multi selection mode or we have
				 * a selection. Otherwise setValue will be called with an empty value
				 * which will cause the change event to fire twice.
				 */
				if (isMulti || hasRecords) {
					me.setValue(selectedRecords, false);
				}
				if (hasRecords) {
					me.trigger('select', me, selectedRecords);
				}
				me.inputEl.focus();
			}
		},

		onTriggerClick : function() {
			var me = this;
			if (!me.readOnly && !me.disabled) {
				if (me.isExpanded) {
					me.collapse();
				} else {
					me.onFocus({});
					if (me.triggerAction === 'all') {
						me.doQuery(me.allQuery, true);
					} else {
						me.doQuery(me.getRawValue(), false, true);
					}
				}
				me.inputEl.focus();
			}
		},

		/**
		 * @private
		 * Set the value of {@link #hiddenDataEl}
		 * Dynamically adds and removes input[type=hidden] elements
		 */
		setHiddenValue : function(values) {
			var me = this, name = me.hiddenName, i, dom, childNodes, input, valueCount, childrenCount;

			if (!me.hiddenDataEl || !name) {
				return;
			}
			values = aries.Array.from(values);
			dom = me.hiddenDataEl.dom;
			childNodes = dom.childNodes;
			input = childNodes[0];
			valueCount = values.length;
			childrenCount = childNodes.length;

			if (!input && valueCount > 0) {
				me.hiddenDataEl.update(Ext.DomHelper.markup({
					tag : 'input',
					type : 'hidden',
					name : name
				}));
				childrenCount = 1;
				input = dom.firstChild;
			}
			while (childrenCount > valueCount) {
				dom.removeChild(childNodes[0]); --childrenCount;
			}
			while (childrenCount < valueCount) {
				dom.appendChild(input.cloneNode(true)); ++childrenCount;
			}
			for ( i = 0; i < valueCount; i++) {
				childNodes[i].value = values[i];
			}
		},

		/**
		 * Sets the specified value(s) into the field. For each value, if a record is found in the {@link #store} that
		 * matches based on the {@link #valueField}, then that record's {@link #displayField} will be displayed in the
		 * field. If no match is found, and the {@link #valueNotFoundText} config option is defined, then that will be
		 * displayed as the default field text. Otherwise a blank value will be shown, although the value will still be set.
		 * @param {String/String[]} value The value(s) to be set. Can be either a single String or {@link Ext.data.Model},
		 * or an Array of Strings or Models.
		 * @return {Ext.form.field.Field} this
		 */
		setValue : function(value, doSelect) {
			var me = this, valueNotFoundText = me.valueNotFoundText, inputEl = me.inputEl, i, len, record, dataObj, matchedRecords = [], displayTplData = [], processedValue = [];

			if (me.store.loading) {
				// Called while the Store is loading. Ensure it is processed by the onLoad method.
				me.value = value;
				me.setHiddenValue(me.value);
				return me;
			}

			// This method processes multi-values, so ensure value is an array.
			value = aries.Array.from(value);

			// Loop through values, matching each from the Store, and collecting matched records
			for ( i = 0, len = value.length; i < len; i++) {
				record = value[i];
				if (!record || !( record instanceof Backbone.Model)) {
					record = me.findRecordByValue(record);
				}
				// record found, select it.
				if (record) {
					matchedRecords.push(record);
					displayTplData.push(record.attributes);
					processedValue.push(record.get(me.valueField));
				}
				// record was not found, this could happen because
				// store is not loaded or they set a value not in the store
				else {
					// If we are allowing insertion of values not represented in the Store, then push the value and
					// create a fake record data object to push as a display value for use by the displayTpl
					if (!me.forceSelection) {
						processedValue.push(value[i]);
						dataObj = {};
						dataObj[me.displayField] = value[i];
						displayTplData.push(dataObj);
						// TODO: Add config to create new records on selection of a value that has no match in the Store
					}
					// Else, if valueNotFoundText is defined, display it, otherwise display nothing for this value
					else if (Ext.isDefined(valueNotFoundText)) {
						displayTplData.push(valueNotFoundText);
					}
				}
			}

			// Set the value of this field. If we are multiselecting, then that is an array.
			me.setHiddenValue(processedValue);
			me.value = me.multiSelect ? processedValue : processedValue[0];
			if (_.isUndefined(me.value)) {
				me.value = null;
			}
			me.displayTplData = displayTplData;
			//store for getDisplayValue method
			me.lastSelection = me.valueModels = matchedRecords;

			if (inputEl && me.emptyText && !_.isEmpty(value)) {
				inputEl.removeCls(me.emptyCls);
			}
			// Calculate raw value from the collection of Model data
			me.setRawValue(me.getDisplayValue());
			me.checkChange();

			if (doSelect !== false) {
				me.syncSelection();
			}
			me.applyEmptyText();

			return me;
		},

		/**
		 * @private Synchronizes the selection in the picker to match the current value of the combobox.
		 */
		syncSelection : function() {
			var me = this, picker = me.picker, selection, selModel, values = me.valueModels || [], vLen = values.length, v, value;

			if (picker) {
				// From the value, find the Models that are in the store's current data
				selection = [];
				for ( v = 0; v < vLen; v++) {
					value = values[v];

					if (value && value.isModel && me.store.indexOf(value) >= 0) {
						selection.push(value);
					}
				}

				// Update the selection to match
				me.ignoreSelection++;
				selModel = picker.getSelectionModel();
				selModel.deselectAll();
				if (selection.length) {
					selModel.select(selection);
				}
				me.ignoreSelection--;
			}
		}
	})).mixin({
		bindable : aries.util.Bindable
	})
})
